package com.aizuda.boot.aspect;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.annotation.Annotation;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.stream.IntStream;

@Slf4j
@Aspect
@Component
@Order(1)
public class ApiMonitorAspect {

    private static final int MAX_LOG_LENGTH = 500;
    private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final boolean LOG_TOKEN = false; // 是否打印 token

    @Pointcut("execution(* com.aizuda.boot.modules..*Controller.*(..))")
    public void controllerMethods() {
    }

    @Before("controllerMethods()")
    public void before(JoinPoint joinPoint) {
        START_TIME.set(System.nanoTime());

        HttpServletRequest request = getHttpServletRequest();
        if (request == null) return;

        Map<String, Object> logMap = new LinkedHashMap<>();
        logMap.put("type", "REQUEST");
        logMap.put("uri", request.getRequestURI());
        logMap.put("method", request.getMethod());
        if (LOG_TOKEN) {
            String token = request.getHeader("accessToken");
            if (token != null && !token.isEmpty()) {
                logMap.put("token", truncate(token));
            }
        }
        List<Map<String, Object>> params = formatArgs(joinPoint);
        if (!params.isEmpty()) {
            logMap.put("params", params);
        }

        log.info(formatLog("START", logMap));
    }

    @AfterReturning(value = "controllerMethods()", returning = "result")
    public void afterReturning(Object result) {
        try {
            Map<String, Object> logMap = new LinkedHashMap<>();
            logMap.put("type", "RESPONSE");
            logMap.put("time_ms", elapsedMillis());
            if (result != null) {
                logMap.put("result", truncate(toJson(result)));
            }

            log.info(formatLog("END", logMap));
        } finally {
            START_TIME.remove();
        }
    }

    @AfterThrowing(value = "controllerMethods()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        try {
            Map<String, Object> logMap = new LinkedHashMap<>();
            logMap.put("type", "EXCEPTION");
            logMap.put("time_ms", elapsedMillis());
            if (ex.getMessage() != null) {
                logMap.put("error", ex.getMessage());
            }

            log.error(formatLog("END", logMap), ex);
        } finally {
            START_TIME.remove();
        }
    }

    private HttpServletRequest getHttpServletRequest() {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return attrs != null ? attrs.getRequest() : null;
    }

    private List<Map<String, Object>> formatArgs(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = signature.getParameterNames();
        Parameter[] parameters = signature.getMethod().getParameters();
        Object[] args = joinPoint.getArgs();

        List<Map<String, Object>> paramList = new ArrayList<>();

        IntStream.range(0, args.length).forEach(i -> {
            Object arg = args[i];
            Parameter parameter = parameters[i];

            if (isFilteredType(parameter.getType(), arg)) return;

            Map<String, Object> paramMap = new LinkedHashMap<>();
            paramMap.put("name", paramNames != null && i < paramNames.length ? paramNames[i] : "arg" + i);
            paramMap.put("source", resolveParamSource(parameter));
            paramMap.put("value", truncate(toJson(arg)));

            paramList.add(paramMap);
        });

        return paramList;
    }

    private String resolveParamSource(Parameter parameter) {
        for (Annotation annotation : parameter.getAnnotations()) {
            if (annotation.annotationType() == RequestParam.class) return "query";
            if (annotation.annotationType() == RequestBody.class) return "body";
            if (annotation.annotationType() == PathVariable.class) return "path";
        }
        return "unknown";
    }

    private String toJson(Object obj) {
        if (obj == null) return "";
        try {
            return OBJECT_MAPPER.writeValueAsString(obj);
        } catch (Exception e) {
            return String.valueOf(obj);
        }
    }

    private String truncate(String value) {
        if (value == null) return "";
        return value.length() <= MAX_LOG_LENGTH ? value : value.substring(0, MAX_LOG_LENGTH) + "...";
    }

    private boolean isFilteredType(Class<?> clazz, Object arg) {
        if (clazz == null) return false;

        String name = clazz.getName();
        if ("org.springframework.web.multipart.MultipartFile".equals(name)
                || "[Lorg.springframework.web.multipart.MultipartFile;".equals(name)) return true;

        if (HttpServletRequest.class.isAssignableFrom(clazz) || HttpServletResponse.class.isAssignableFrom(clazz)) {
            return true;
        }

        if (arg instanceof Iterable<?>) {
            for (Object item : (Iterable<?>) arg) {
                if (item != null && "org.springframework.web.multipart.MultipartFile".equals(item.getClass().getName())) {
                    return true;
                }
            }
        }
        return false;
    }

    private long elapsedMillis() {
        Long start = START_TIME.get();
        return start == null ? 0 : (System.nanoTime() - start) / 1_000_000;
    }

    /**
     * 格式化日志输出
     */
    private String formatLog(String phase, Map<String, Object> logMap) {
        StringBuilder sb = new StringBuilder();
        sb.append("\n================= API LOG ").append(phase).append(" =================\n");
        logMap.forEach((k, v) -> {
            if (v != null && !"".equals(v.toString().trim())) {
                sb.append(k).append(": ").append(v).append(" | ");
            }
        });
        if (sb.lastIndexOf(" | ") == sb.length() - 3) {
            sb.delete(sb.length() - 3, sb.length());
        }
        sb.append("\n============================================");
        return sb.toString();
    }
}
